use crate::msg::{
    ExecuteMsg, InstantiateMsg, PoolInfoResponse, QueryMsg, Token, TokenAForTokenBPriceResponse,
    TokenBForTokenAPriceResponse, TokenSelect,
};
use crate::state::{
    read_config, read_pool_by_address, read_pools, read_tr_fees, set_tr_fees, store_config,
    store_pool, ConfigInfo, FeesType, PoolInfo,
};
use crate::u256_math::{div, mul};
use cosmwasm_std::{
    log, to_binary, Api, BankMsg, Binary, Coin, CosmosMsg, Env, Extern, HandleResponse,
    HandleResult, HumanAddr, InitResponse, Querier, StdError, StdResult, Storage, Uint128, WasmMsg,
};
use hermit_toolkit_hmip21::asset::AssetInfo;
use hermit_toolkit_hmip21::{
    burn_from_msg, mint_msg, token_info_query, transfer_from_msg, transfer_msg, InitConfig,
    InitHook, InitMsg, InitialBalance, balance_query,
};
use primitive_types::U256;
use std::ops::{Add, Sub};

const FEE_DECIMAL: u64 = 10000;
const BLOCK_SIZE: usize = 256;

// Note, you can use StdResult in some functions where you do not make use of the custom errors
pub fn init<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    msg: InstantiateMsg,
) -> StdResult<InitResponse> {
    let owner = msg.owner.unwrap_or(env.message.sender);
    let total_fee_percent = msg.lp_fee_percent + msg.protocol_fee_percent + msg.dao_fee_percent;
    if total_fee_percent > FEE_DECIMAL {
        return Err(StdError::generic_err("protocol_fee_percent set error"));
    }
    let config = ConfigInfo {
        owner,
        lp_token_code_id: msg.lp_token_code_id,
        lp_token_code_hash: msg.lp_token_code_hash,
        lp_fee_percent: msg.lp_fee_percent,
        protocol_fee_percent: msg.protocol_fee_percent,
        protocol_fee_recipient: msg.protocol_fee_recipient,
        dao_fee_recipient: msg.dao_fee_recipient,
        dao_fee_percent: msg.dao_fee_percent,
        lp_total_supply: Uint128::zero(),
    };

    store_config(&mut deps.storage, &config)?;

    Ok(InitResponse {
        messages: vec![],
        log: vec![log("status", "success")], // See https://github.com/CosmWasm/wasmd/pull/386
    })
}

// And declare a custom Error variant for the ones where you will want to make use of it
pub fn handle<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    msg: ExecuteMsg,
) -> HandleResult {
    match msg {
        ExecuteMsg::AddPool {
            token_a_asset,
            token_b_asset,
        } => execute_add_pool(deps, env, token_a_asset, token_b_asset),
        ExecuteMsg::Register {
            token_a_address,
            token_b_address,
        } => execute_register(deps, env, token_a_address, token_b_address),
        ExecuteMsg::AddLiquidity {
            token_a_address,
            token_b_address,
            token_a_amount,
            min_liquidity,
            max_token_b,
        } => execute_add_liquidity(
            deps,
            env,
            token_a_address,
            token_b_address,
            min_liquidity,
            token_a_amount,
            max_token_b,
        ),
        ExecuteMsg::RemoveLiquidity {
            token_a_address,
            token_b_address,
            amount,
            min_token_a,
            min_token_b,
        } => execute_remove_liquidity(
            deps,
            env,
            token_a_address,
            token_b_address,
            amount,
            min_token_a,
            min_token_b,
        ),
        ExecuteMsg::Swap {
            token_a_address,
            token_b_address,
            input_amount,
            min_output,
            ..
        } => execute_swap(
            deps,
            env.clone(),
            token_a_address,
            token_b_address,
            input_amount,
            env.message.sender,
            min_output,
        ),
        ExecuteMsg::SwapAndSendTo {
            token_a_address,
            token_b_address,
            input_amount,
            recipient,
            min_output,
        } => execute_swap(
            deps,
            env,
            token_a_address,
            token_b_address,
            input_amount,
            recipient,
            min_output,
        ),
        ExecuteMsg::CollectTransferFees {
            user_address,
            token_address,
            transfer_fees,
        } => execute_collect_transfer_fees(deps, env, user_address, token_address, transfer_fees),
        ExecuteMsg::UpdateConfig { config } => execute_update_config(deps, env, config),
        ExecuteMsg::SetTrFees {
            token_address,
            in_fees,
            out_fees,
        } => execute_set_tr_fees(deps, env, token_address, in_fees, out_fees),
    }
}

pub fn execute_add_pool<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    token_a_asset: AssetInfo,
    token_b_asset: AssetInfo,
) -> HandleResult {
    // load config info
    let config = read_config(&deps.storage)?;
    // get token_a_address and token_b_address from asset
    let token_a_address = match token_a_asset.clone() {
        AssetInfo::Token { contract_addr, .. } => contract_addr,
        AssetInfo::NativeToken { denom, .. } => HumanAddr::from(denom),
    };
    let token_b_address = match token_b_asset.clone() {
        AssetInfo::Token { contract_addr, .. } => contract_addr,
        AssetInfo::NativeToken { denom, .. } => HumanAddr::from(denom),
    };
    // check token_a/token_b pool is exixts
    if read_pool_by_address(&deps.storage, &token_a_address, &token_b_address).is_ok()
        || read_pool_by_address(&deps.storage, &token_b_address, &token_a_address).is_ok()
    {
        return Err(StdError::generic_err("Pool is exist"));
    }
    // store pool info
    let mut pair_info: Vec<HumanAddr> = Vec::new();
    pair_info.push(token_a_address.clone());
    pair_info.push(token_b_address.clone());
    pair_info.sort_by(|a, b| a.0.as_bytes().cmp(&b.0.as_bytes()));
    store_pool(
        &mut deps.storage,
        &pair_info[0],
        &pair_info[1],
        &PoolInfo {
            token_a_asset: token_a_asset.clone(),
            token_a_reserve: Uint128::zero(),
            token_b_asset: token_b_asset.clone(),
            token_b_reserve: Uint128::zero(),
            lp_token_asset: token_a_asset.clone(),
            creator: env.message.sender.clone(),
            create_block: env.block.height,
        },
        true,
    )?;
    // Instantiate LP token Contract
    let messages: Vec<CosmosMsg> = vec![CosmosMsg::Wasm(WasmMsg::Instantiate {
        code_id: config.lp_token_code_id,
        callback_code_hash: config.lp_token_code_hash,
        label: format!(
            "HermitSwap Liquidity Provider (LP) token for {}-{}-{}",
            token_a_address, token_b_address, env.block.height
        ),
        msg: to_binary(&InitMsg {
            name: "HermitLP".to_string(),
            admin: Some(env.contract.address.clone()),
            symbol: "HMLP".to_string(),
            decimals: 18,
            initial_balances: Some(vec![InitialBalance {
                address: env.message.sender.clone(),
                amount: Uint128::zero(),
            }]),
            prng_seed: Binary::from("".as_bytes()),
            config: Some(InitConfig {
                public_total_supply: Some(true),
                enable_deposit: Some(true),
                enable_redeem: Some(true),
                enable_mint: Some(true),
                enable_burn: Some(true),
            }),
            init_hook: Some(InitHook {
                msg: to_binary(&ExecuteMsg::Register {
                    token_a_address,
                    token_b_address,
                })?,
                contract_addr: env.contract.address.clone(),
                code_hash: env.contract_code_hash,
            }),
        })?,
        send: vec![],
    })];

    Ok(HandleResponse {
        messages,
        log: vec![
            log("action", "create_pair"),
            log("pair", format!("{}-{}", token_a_asset, token_b_asset)),
        ],
        data: None,
    })
}

// alfer create pair call back registe lp_token
pub fn execute_register<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    token_a_address: HumanAddr,
    token_b_address: HumanAddr,
) -> HandleResult {
    // load config info
    let config = read_config(&deps.storage)?;
    // check token_a/token_b pool is exixts
    let mut pool_map = read_pool_by_address(&deps.storage, &token_a_address, &token_b_address);
    if !pool_map.is_ok() {
        pool_map = read_pool_by_address(&deps.storage, &token_b_address, &token_a_address);
        if !pool_map.is_ok() {
            return Err(StdError::generic_err("Pool is not exist"));
        }
    }
    let pool_info = pool_map.unwrap();
    if pool_info.clone().lp_token_asset != pool_info.clone().token_a_asset
        && pool_info.clone().lp_token_asset != pool_info.clone().token_b_asset
    {
        return Err(StdError::generic_err("Pair was already registered"));
    };
    // store lp_token_asset info
    let mut pair_info: Vec<HumanAddr> = Vec::new();
    pair_info.push(token_a_address.clone());
    pair_info.push(token_b_address.clone());
    pair_info.sort_by(|a, b| a.0.as_bytes().cmp(&b.0.as_bytes()));
    store_pool(
        &mut deps.storage,
        &pair_info[0],
        &pair_info[1],
        &PoolInfo {
            lp_token_asset: AssetInfo::Token {
                contract_addr: env.message.sender.clone(),
                token_code_hash: config.lp_token_code_hash,
                token_decimal: 18u32,
            },
            ..pool_info
        },
        false,
    )?;

    Ok(HandleResponse {
        messages: vec![],
        log: vec![
            log("action", "register"),
            log("pair_contract_addr", env.message.sender),
        ],
        data: None,
    })
}

pub fn execute_add_liquidity<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    token_a_address: HumanAddr,
    token_b_address: HumanAddr,
    min_liquidity: Uint128,
    token_a_amount: Uint128,
    max_token_b: Uint128,
) -> HandleResult {
    // load config info
    let config = read_config(&deps.storage)?;
    // check token_a/token_b pool is exixts
    let mut pool_map = read_pool_by_address(&deps.storage, &token_a_address, &token_b_address);
    if !pool_map.is_ok() {
        pool_map = read_pool_by_address(&deps.storage, &token_b_address, &token_a_address);
        if !pool_map.is_ok() {
            return Err(StdError::generic_err("Pool is not exist"));
        }
    }
    let mut pool_info = pool_map.unwrap();
    let liquidity_token = match pool_info.clone().lp_token_asset {
        AssetInfo::Token { contract_addr, .. } => contract_addr,
        AssetInfo::NativeToken { denom: _ } => HumanAddr::from(""),
    };
    // validate funds
    validate_input_amount(
        &env.message.sent_funds,
        token_a_amount,
        &pool_info.token_a_asset,
    )?;
    validate_input_amount(
        &env.message.sent_funds,
        max_token_b,
        &pool_info.token_b_asset,
    )?;

    let lp_token_supply = query_lp_supply(&deps, &liquidity_token, &config.lp_token_code_hash)?;
    let liquidity_amount =
        get_lp_token_amount_to_mint(token_a_amount, lp_token_supply, pool_info.token_a_reserve);

    let token_b_amount = get_token_b_amount_required(
        max_token_b,
        token_a_amount,
        lp_token_supply,
        pool_info.token_b_reserve,
        pool_info.token_a_reserve,
    )?;

    if liquidity_amount < min_liquidity {
        return Err(StdError::generic_err(format!(
            "Min liquidity error: requested: {}, available: {}",
            min_liquidity, liquidity_amount
        )));
    }
    if token_b_amount > max_token_b {
        return Err(StdError::generic_err(format!(
            "Max token error: max_token: {}, tokens_required: {}",
            max_token_b, token_b_amount
        )));
    }

    // Generate hmip20 transfer messages if necessary
    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    match pool_info.clone().token_a_asset {
        AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } => {
            if contract_addr.clone() == token_a_address {
                transfer_msgs.push(transfer_from_msg(
                    env.message.sender.clone(),
                    env.contract.address.clone(),
                    token_a_amount,
                    None,
                    None,
                    BLOCK_SIZE,
                    token_code_hash.clone().into(),
                    contract_addr.clone().into(),
                )?);
            }
            if contract_addr == token_b_address {
                transfer_msgs.push(transfer_from_msg(
                    env.message.sender.clone(),
                    env.contract.address.clone(),
                    token_b_amount,
                    None,
                    None,
                    BLOCK_SIZE,
                    token_code_hash.into(),
                    contract_addr.into(),
                )?);
            }
        }
        _ => (),
    }
    match pool_info.clone().token_b_asset {
        AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } => {
            if contract_addr.clone() == token_a_address {
                transfer_msgs.push(transfer_from_msg(
                    env.message.sender.clone(),
                    env.contract.address.clone(),
                    token_a_amount,
                    None,
                    None,
                    BLOCK_SIZE,
                    token_code_hash.clone().into(),
                    contract_addr.clone().into(),
                )?);
            }
            if contract_addr == token_b_address {
                transfer_msgs.push(transfer_from_msg(
                    env.message.sender.clone(),
                    env.contract.address.clone(),
                    token_b_amount,
                    None,
                    None,
                    BLOCK_SIZE,
                    token_code_hash.into(),
                    contract_addr.into(),
                )?);
            }
        }
        _ => (),
    }
    // update pool info
    pool_info.token_a_reserve += token_a_amount;
    pool_info.token_b_reserve += token_b_amount;

    let mut pair_info: Vec<HumanAddr> = Vec::new();
    pair_info.push(token_a_address.clone());
    pair_info.push(token_b_address.clone());
    pair_info.sort_by(|a, b| a.0.as_bytes().cmp(&b.0.as_bytes()));
    store_pool(
        &mut deps.storage,
        &pair_info[0],
        &pair_info[1],
        &pool_info,
        false,
    )?;

    // Generate mint lp token messages
    transfer_msgs.push(mint_msg(
        env.message.sender.clone(),
        liquidity_amount,
        None,
        None,
        BLOCK_SIZE,
        config.lp_token_code_hash,
        liquidity_token,
    )?);

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "add_liquidity"),
            log("assets",format!("token_a_address: {}, token_a_amount: {}, token_b_address: {},token_b_amount: {}, liquidity_amount: {}", token_a_address, token_a_amount, token_b_address, token_b_amount, liquidity_amount)),
        ],
        data: None,
    })
}

pub fn execute_remove_liquidity<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    token_a_address: HumanAddr,
    token_b_address: HumanAddr,
    amount: Uint128,
    min_token_a: Uint128,
    min_token_b: Uint128,
) -> HandleResult {
    // load config info
    let config = read_config(&deps.storage)?;
    // check token_a/token_b pool is exixts
    let mut pool_map = read_pool_by_address(&deps.storage, &token_a_address, &token_b_address);
    if !pool_map.is_ok() {
        pool_map = read_pool_by_address(&deps.storage, &token_b_address, &token_a_address);
        if !pool_map.is_ok() {
            return Err(StdError::generic_err("Pool is not exist"));
        }
    }
    let mut pool_info = pool_map.unwrap();
    // get token_a_address and token_b_address from asset
    let (token_a_address, token_a_decimal) = match pool_info.clone().token_a_asset {
        AssetInfo::Token {
            contract_addr,
            token_decimal,
            ..
        } => (contract_addr, token_decimal),
        AssetInfo::NativeToken { denom, .. } => (HumanAddr::from(denom), 6),
    };
    let (token_b_address, token_b_decimal) = match pool_info.clone().token_b_asset {
        AssetInfo::Token {
            contract_addr,
            token_decimal,
            ..
        } => (contract_addr, token_decimal),
        AssetInfo::NativeToken { denom, .. } => (HumanAddr::from(denom), 6),
    };
    let lp_token_address = match pool_info.clone().lp_token_asset {
        AssetInfo::Token { contract_addr, .. } => contract_addr,
        AssetInfo::NativeToken { denom: _ } => HumanAddr::from(""),
    };
    // check lp token balance of user account
    // let lp_token_balance = get_token_balance(
    //     &deps,
    //     &lp_token_address,
    //     &config.lp_token_code_hash,
    //     &env.message.sender,
    //     &lp_token_view_key,
    // )?;
    // if amount > lp_token_balance {
    //     return Err(StdError::generic_err(format!(
    //         "Insufficient liquidity error: requested: {}, available: {}",
    //         amount, lp_token_balance
    //     )));
    // }
    // calculate token_a_amount
    let lp_token_supply = query_lp_supply(&deps, &lp_token_address, &config.lp_token_code_hash)?;
    let token_a_amount = amount
        .multiply_ratio(1u64, 10u128.pow(token_a_decimal - 4))
        .multiply_ratio(pool_info.token_a_reserve, lp_token_supply)
        .multiply_ratio(10u128.pow(token_a_decimal - 4), 1u64);
    // let token_a_amount = Uint128(
    //     div(
    //         mul(
    //             Some(U256::from(amount.u128())),
    //             Some(U256::from(pool_info.token_a_reserve.u128())),
    //         ),
    //         Some(U256::from(lp_token_supply.u128())),
    //     )
    //     .unwrap()
    //     .low_u128(),
    // );
    if token_a_amount < min_token_a {
        return Err(StdError::generic_err(format!(
            "Min token_a error: requested: {}, available: {}",
            min_token_a, token_a_amount
        )));
    }
    // calculate token_b_amount
    let token_b_amount = amount
        .multiply_ratio(1u64, 10u128.pow(token_b_decimal - 4))
        .multiply_ratio(pool_info.token_b_reserve, lp_token_supply)
        .multiply_ratio(10u128.pow(token_b_decimal - 4), 1u64);
    // let token_b_amount = Uint128(
    //     div(
    //         mul(
    //             Some(U256::from(amount.u128())),
    //             Some(U256::from(pool_info.token_b_reserve.u128())),
    //         ),
    //         Some(U256::from(lp_token_supply.u128())),
    //     )
    //     .unwrap()
    //     .low_u128(),
    // );
    if token_b_amount < min_token_b {
        return Err(StdError::generic_err(format!(
            "Min token_b error: requested: {}, available: {}",
            min_token_b, token_b_amount
        )));
    }
    // update pool_info
    pool_info.token_a_reserve = pool_info.token_a_reserve.sub(token_a_amount)?;
    pool_info.token_b_reserve = pool_info.token_b_reserve.sub(token_b_amount)?;

    let mut pair_info: Vec<HumanAddr> = Vec::new();
    pair_info.push(token_a_address.clone());
    pair_info.push(token_b_address.clone());
    pair_info.sort_by(|a, b| a.0.as_bytes().cmp(&b.0.as_bytes()));
    store_pool(
        &mut deps.storage,
        &pair_info[0],
        &pair_info[1],
        &pool_info,
        false,
    )?;

    // transfer token_a and token_b
    // Generate hmip20 transfer messages if necessary
    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    match pool_info.token_a_asset {
        AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } => {
            transfer_msgs.push(transfer_msg(
                env.message.sender.clone(),
                token_a_amount,
                None,
                None,
                BLOCK_SIZE,
                token_code_hash.into(),
                contract_addr.into(),
            )?);
        }
        AssetInfo::NativeToken { denom, .. } => {
            transfer_msgs.push(CosmosMsg::Bank(BankMsg::Send {
                from_address: env.contract.address.clone(),
                to_address: env.message.sender.clone(),
                amount: vec![Coin::new(token_a_amount.u128(), denom.as_str())],
            }));
        }
    }
    match pool_info.token_b_asset {
        AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } => {
            transfer_msgs.push(transfer_msg(
                env.message.sender.clone(),
                token_b_amount,
                None,
                None,
                BLOCK_SIZE,
                token_code_hash.into(),
                contract_addr.into(),
            )?);
        }
        AssetInfo::NativeToken { denom, .. } => {
            transfer_msgs.push(CosmosMsg::Bank(BankMsg::Send {
                from_address: env.contract.address,
                to_address: env.message.sender.clone(),
                amount: vec![Coin::new(token_b_amount.u128(), denom.as_str())],
            }));
        }
    }
    // Generate burn lp token messages
    transfer_msgs.push(burn_from_msg(
        env.message.sender.clone(),
        amount,
        None,
        None,
        BLOCK_SIZE,
        config.lp_token_code_hash,
        lp_token_address,
    )?);

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "remove_liquidity"),
            log(
                "assets",
                format!(
                    "token_a_address: {}, token_b_address:{}, amount: {}",
                    token_a_address, token_b_address, amount
                ),
            ),
        ],
        data: None,
    })
}

#[allow(clippy::too_many_arguments)]
pub fn execute_swap<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    token_a_address: HumanAddr,
    token_b_address: HumanAddr,
    input_amount: Uint128,
    recipient: HumanAddr,
    min_output: Uint128,
) -> HandleResult {
    let mut input_token_enum = TokenSelect::TokenA;
    // load config info
    let config = read_config(&deps.storage)?;
    // check token_a/token_b pool is exixts
    let mut pool_map = read_pool_by_address(&deps.storage, &token_a_address, &token_b_address);
    if !pool_map.is_ok() {
        pool_map = read_pool_by_address(&deps.storage, &token_b_address, &token_a_address);
        if !pool_map.is_ok() {
            return Err(StdError::generic_err("Pool is not exist"));
        }
    }

    let mut pool_info = pool_map.unwrap();
    let pool_a_address = match pool_info.token_a_asset.clone() {
        AssetInfo::Token { contract_addr, .. } => contract_addr,
        AssetInfo::NativeToken { denom: _ } => HumanAddr::from(""),
    };
    if token_b_address == pool_a_address {
        input_token_enum = TokenSelect::TokenB;
    }
    let input_token = match input_token_enum {
        TokenSelect::TokenA => Token {
            asset: pool_info.clone().token_a_asset,
            reserve: pool_info.clone().token_a_reserve,
        },
        TokenSelect::TokenB => Token {
            asset: pool_info.clone().token_b_asset,
            reserve: pool_info.clone().token_b_reserve,
        },
    };
    let output_token = match input_token_enum {
        TokenSelect::TokenA => Token {
            asset: pool_info.clone().token_b_asset,
            reserve: pool_info.token_b_reserve,
        },
        TokenSelect::TokenB => Token {
            asset: pool_info.clone().token_a_asset,
            reserve: pool_info.clone().token_a_reserve,
        },
    };

    // validate input_amount if native input token
    validate_input_amount(
        &env.message.sent_funds,
        input_amount,
        &pool_info.token_a_asset,
    )?;

    let tr_in_fees = read_tr_fees(&deps.storage, &token_a_address, FeesType::In);
    let tr_out_fees = read_tr_fees(&deps.storage, &token_b_address, FeesType::Out);

    let total_fee_percent =
        config.lp_fee_percent + config.protocol_fee_percent + config.dao_fee_percent + tr_in_fees;
    let mut token_out_amount = get_token_out_amount(
        input_amount,
        input_token.reserve,
        output_token.reserve,
        total_fee_percent,
    )?;
    if tr_out_fees > 0 {
        token_out_amount = token_out_amount.multiply_ratio(FEE_DECIMAL - tr_out_fees, FEE_DECIMAL)
    }

    if min_output > token_out_amount {
        return Err(StdError::generic_err(format!(
            "Swap min_output error: min_output: {}, available: {}",
            min_output, token_out_amount
        )));
    }

    // Calculate fees
    let (total_fee_amount, _tr_fee_amount, protocol_fee_amount, dao_fee_amount) = get_fee_amount(
        input_amount,
        config.protocol_fee_percent,
        config.dao_fee_percent,
        tr_in_fees,
    )?;
    let input_amount_without_fee = input_amount
        .sub(Uint128::from(protocol_fee_amount))?
        .sub(Uint128::from(dao_fee_amount))?;
    // Generate hmip20 transfer messages if necessary
    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    if let AssetInfo::Token {
        contract_addr,
        token_code_hash,
        ..
    } = &input_token.asset
    {
        transfer_msgs.push(transfer_from_msg(
            env.message.sender.clone(),
            env.contract.address.clone(),
            input_amount_without_fee,
            None,
            None,
            BLOCK_SIZE,
            token_code_hash.into(),
            contract_addr.into(),
        )?);
    }

    // Send protocol fee to protocol fee recipient
    if !total_fee_amount.is_zero() {
        match input_token.asset {
            AssetInfo::Token {
                contract_addr,
                token_code_hash,
                ..
            } => {
                if !protocol_fee_amount.is_zero() {
                    transfer_msgs.push(transfer_from_msg(
                        env.message.sender.clone(),
                        config.protocol_fee_recipient,
                        protocol_fee_amount,
                        None,
                        None,
                        BLOCK_SIZE,
                        token_code_hash.clone().into(),
                        contract_addr.clone().into(),
                    )?);
                }
                if !dao_fee_amount.is_zero() {
                    transfer_msgs.push(transfer_from_msg(
                        env.message.sender,
                        config.dao_fee_recipient,
                        dao_fee_amount,
                        None,
                        None,
                        BLOCK_SIZE,
                        token_code_hash.into(),
                        contract_addr.into(),
                    )?);
                }
            }
            AssetInfo::NativeToken { denom, .. } => {
                if !protocol_fee_amount.is_zero() {
                    transfer_msgs.push(CosmosMsg::Bank(BankMsg::Send {
                        from_address: env.contract.address.clone(),
                        to_address: config.protocol_fee_recipient,
                        amount: vec![Coin::new(protocol_fee_amount.u128(), denom.as_str())],
                    }));
                }
                if !dao_fee_amount.is_zero() {
                    transfer_msgs.push(CosmosMsg::Bank(BankMsg::Send {
                        from_address: env.contract.address.clone(),
                        to_address: config.dao_fee_recipient,
                        amount: vec![Coin::new(dao_fee_amount.u128(), denom.as_str())],
                    }));
                }
            }
        }
    }
    // Send tokenout to sender
    match output_token.asset {
        AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } => {
            transfer_msgs.push(transfer_msg(
                recipient,
                token_out_amount,
                None,
                None,
                BLOCK_SIZE,
                token_code_hash.into(),
                contract_addr.into(),
            )?);
        }
        AssetInfo::NativeToken { denom, .. } => {
            transfer_msgs.push(CosmosMsg::Bank(BankMsg::Send {
                from_address: env.contract.address.clone(),
                to_address: recipient,
                amount: vec![Coin::new(token_out_amount.u128(), denom.as_str())],
            }));
        }
    }
    // update pool info
    let mut pair_info: Vec<HumanAddr> = Vec::new();
    pair_info.push(token_a_address.clone());
    pair_info.push(token_b_address.clone());
    pair_info.sort_by(|a, b| a.0.as_bytes().cmp(&b.0.as_bytes()));
    if token_a_address == pool_a_address {
        pool_info.token_a_reserve += input_amount_without_fee;
        pool_info.token_b_reserve = pool_info.token_b_reserve.sub(token_out_amount)?;
        store_pool(
            &mut deps.storage,
            &pair_info[0],
            &pair_info[1],
            &pool_info,
            false,
        )?;
    } else {
        pool_info.token_b_reserve += input_amount_without_fee;
        pool_info.token_a_reserve = pool_info.token_a_reserve.sub(token_out_amount)?;
        store_pool(
            &mut deps.storage,
            &pair_info[0],
            &pair_info[1],
            &pool_info,
            false,
        )?;
    }

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "swap"),
            log(
                "assets",
                format!(
                    "input_token_address: {}, input_amount: {}, output_token_address: {}, output_token_amount: {}",
                    token_a_address, input_amount, token_b_address, token_out_amount
                ),
            ),
        ],
        data: None,
    })
}

pub fn execute_collect_transfer_fees<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    user_address: HumanAddr,
    token_address: HumanAddr,
    transfer_fees: Uint128,
) -> HandleResult {
    // find token pools
    let all_pools: Vec<PoolInfo> = read_pools(&deps)?;
    let mut is_token_a: bool = true;
    let mut token_a_address: HumanAddr = env.message.sender.clone();
    let mut token_b_address: HumanAddr = env.message.sender.clone();
    let mut token_a_pools: Vec<PoolInfo> = Vec::new();
    let mut token_b_pools: Vec<PoolInfo> = Vec::new();
    let mut token_pool: PoolInfo = all_pools.iter().next().unwrap().clone();
    let mut token_hash_code = "";
    all_pools.iter().for_each(|pool| {
        match &pool.token_a_asset {
            AssetInfo::Token { contract_addr, .. } => {
                if contract_addr.clone() == token_address {
                    token_a_pools.push(pool.clone())
                }
            }
            _ => (),
        }
        match &pool.token_b_asset {
            AssetInfo::Token { contract_addr, .. } => {
                if contract_addr.clone() == token_address {
                    token_b_pools.push(pool.clone())
                }
            }
            _ => {}
        }
    });
    token_a_pools.sort_by(|a, b| b.token_a_reserve.cmp(&a.token_a_reserve));
    token_b_pools.sort_by(|a, b| b.token_b_reserve.cmp(&a.token_b_reserve));
    if token_a_pools.is_empty() && token_b_pools.is_empty() {
        return Err(StdError::generic_err("Token Pool is not exist"));
    }
    if !token_a_pools.is_empty() && token_b_pools.is_empty() {
        token_pool = token_a_pools.iter().next().unwrap().clone();
        is_token_a = true;
    }
    if !token_b_pools.is_empty() && token_a_pools.is_empty() {
        token_pool = token_b_pools.iter().next().unwrap().clone();
        is_token_a = false;
    }
    if !token_a_pools.is_empty() && !token_b_pools.is_empty() {
        if token_a_pools.iter().next().unwrap().token_a_reserve
            > token_b_pools.iter().next().unwrap().token_b_reserve
        {
            token_pool = token_a_pools.iter().next().unwrap().clone();
            is_token_a = true;
        } else {
            token_pool = token_b_pools.iter().next().unwrap().clone();
            is_token_a = false;
        }
    }

    // collect transfer fees
    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    if is_token_a {
        if let AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } = &token_pool.token_a_asset
        {
            transfer_msgs.push(transfer_from_msg(
                user_address.clone(),
                env.contract.address.clone(),
                transfer_fees,
                None,
                None,
                BLOCK_SIZE,
                token_code_hash.into(),
                contract_addr.into(),
            )?);
            token_a_address = contract_addr.clone();
            token_hash_code = token_code_hash;
        }
        if let AssetInfo::Token { contract_addr, .. } = &token_pool.clone().token_b_asset {
            token_b_address = contract_addr.clone();
        }
    } else {
        if let AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } = &token_pool.token_b_asset
        {
            transfer_msgs.push(transfer_from_msg(
                user_address.clone(),
                env.contract.address.clone(),
                transfer_fees,
                None,
                None,
                BLOCK_SIZE,
                token_code_hash.into(),
                contract_addr.into(),
            )?);
            token_b_address = contract_addr.clone();
            token_hash_code = token_code_hash;
        }
        if let AssetInfo::Token { contract_addr, .. } = &token_pool.clone().token_a_asset {
            token_a_address = contract_addr.clone();
        }
    }
    // update pool info
    if is_token_a {
        token_pool.token_a_reserve += transfer_fees;
    } else {
        token_pool.token_b_reserve += transfer_fees;
    }

    let mut pair_info: Vec<HumanAddr> = Vec::new();
    pair_info.push(token_a_address.clone());
    pair_info.push(token_b_address.clone());
    pair_info.sort_by(|a, b| a.0.as_bytes().cmp(&b.0.as_bytes()));
    store_pool(
        &mut deps.storage,
        &pair_info[0],
        &pair_info[1],
        &token_pool,
        false,
    )?;

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "collect_transfer_fees"),
            log(
                "assets",
                format!(
                    "user_address: {}, token_address: {}, token_a_address: {}, token_b_address: {}, token_hash_code: {},transfer_fees: {}",
                    user_address, token_address, token_a_address, token_b_address, token_hash_code, transfer_fees
                ),
            ),
        ],
        data: None,
    })
}

pub fn execute_update_config<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    config: ConfigInfo,
) -> HandleResult {
    // load config info
    let cur_config = read_config(&deps.storage)?;
    // authorize owner
    if env.message.sender != cur_config.owner {
        return Err(StdError::unauthorized());
    }

    store_config(&mut deps.storage, &config)?;

    Ok(HandleResponse {
        messages: vec![],
        log: vec![log("action", "update_config")],
        data: None,
    })
}

pub fn execute_set_tr_fees<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    token_address: HumanAddr,
    in_fees: u64,
    out_fees: u64,
) -> HandleResult {
    // load config info
    let cur_config = read_config(&deps.storage)?;
    // authorize owner
    if env.message.sender != cur_config.owner {
        return Err(StdError::unauthorized());
    }
    // set_tr_fees
    let _ = set_tr_fees(&mut deps.storage, &token_address, FeesType::In, in_fees);
    let _ = set_tr_fees(&mut deps.storage, &token_address, FeesType::Out, out_fees);

    Ok(HandleResponse {
        messages: vec![],
        log: vec![log("action", "set_tr_fees")],
        data: None,
    })
}

fn get_lp_token_amount_to_mint(
    token_a_amount: Uint128,
    liquidity_supply: Uint128,
    token_a_reserve: Uint128,
) -> Uint128 {
    if liquidity_supply == Uint128::zero() {
        Uint128::from(10u128.pow(18))
    } else {
        token_a_amount
            .multiply_ratio(1u64, 10u128.pow(8 - 4))
            .multiply_ratio(liquidity_supply, token_a_reserve)
            .multiply_ratio(10u128.pow(8 - 4), 1u64)
        // Uint128(
        //     div(
        //         mul(
        //             Some(U256::from(token_a_amount.u128())),
        //             Some(U256::from(liquidity_supply.u128())),
        //         ),
        //         Some(U256::from(token_a_reserve.u128())),
        //     )
        //     .unwrap()
        //     .low_u128(),
        // )
    }
}

fn get_token_b_amount_required(
    max_token: Uint128,
    token_a_amount: Uint128,
    liquidity_supply: Uint128,
    token_b_reserve: Uint128,
    token_a_reserve: Uint128,
) -> Result<Uint128, StdError> {
    if liquidity_supply == Uint128::zero() {
        Ok(max_token)
    } else {
        Ok(token_a_amount
            .multiply_ratio(1u64, 10u128.pow(8 - 4))
            .multiply_ratio(token_b_reserve, 1u64)
            .multiply_ratio(10u128.pow(8 - 4), token_a_reserve)
            .add(Uint128::from(1u64)))
        // Ok(Uint128(
        //     div(
        //         mul(
        //             Some(U256::from(token_a_amount.u128())),
        //             Some(U256::from(token_b_reserve.u128())),
        //         ),
        //         Some(U256::from(token_a_reserve.u128())),
        //     )
        //     .unwrap()
        //     .low_u128(),
        // ))
    }
}

fn validate_input_amount(
    actual_funds: &[Coin],
    given_amount: Uint128,
    given_asset: &AssetInfo,
) -> HandleResult {
    match given_asset {
        AssetInfo::Token { .. } => Ok(HandleResponse::default()),
        AssetInfo::NativeToken { denom } => {
            let actual = get_amount_for_asset(actual_funds, denom);
            if actual.amount < given_amount {
                return Err(StdError::generic_err(format!(
                    "InsufficientFunds : provided: {}, required: {}",
                    actual.amount, given_amount,
                )));
            }
            if &actual.denom != denom {
                return Err(StdError::generic_err(format!(
                    "Incorrect native asset: provided: {}, required: {}",
                    actual.denom, denom
                )));
            };
            Ok(HandleResponse::default())
        }
    }
}

// Calculate input based on output
// uint numerator = reserveIn.mul(amountOut).mul(10000);
// uint denominator = reserveOut.sub(amountOut).mul(997);
// amountIn = (numerator / denominator).add(1);
// Calculate output based on input
// uint amountInWithFee = amountIn.mul(997);
// uint numerator = amountInWithFee.mul(reserveOut);
// uint denominator = reserveIn.mul(10000).add(amountInWithFee);
// amountOut = numerator / denominator;
fn get_token_out_amount(
    input_amount: Uint128,
    input_reserve: Uint128,
    output_reserve: Uint128,
    fee_percent: u64,
) -> StdResult<Uint128> {
    if input_reserve == Uint128::zero() || output_reserve == Uint128::zero() {
        return Err(StdError::generic_err("No liquidity"));
    };

    let fee_reduction_percent = FEE_DECIMAL - fee_percent;
    let input_amount_with_fee =
        Uint128::from(input_amount.multiply_ratio(fee_reduction_percent, 1u64));
    let numerator = input_amount_with_fee.multiply_ratio(output_reserve, 1u64);
    let assetinator = input_reserve
        .multiply_ratio(FEE_DECIMAL, 1u64)
        .add(input_amount_with_fee);

    Ok(numerator.multiply_ratio(1u64, assetinator))
}

fn get_fee_amount(
    input_amount: Uint128,
    protocol_fee_percent: u64,
    dao_fee_percent: u64,
    tr_fees: u64,
) -> StdResult<(Uint128, Uint128, Uint128, Uint128)> {
    if (protocol_fee_percent + dao_fee_percent + tr_fees) == 0 {
        return Ok((
            Uint128::zero(),
            Uint128::zero(),
            Uint128::zero(),
            Uint128::zero(),
        ));
    }
    let tr_fee_amount = input_amount.multiply_ratio(tr_fees, FEE_DECIMAL);
    let total_fee_amount = input_amount.multiply_ratio(
        protocol_fee_percent + dao_fee_percent + tr_fees,
        FEE_DECIMAL,
    );

    Ok((
        total_fee_amount,
        tr_fee_amount,
        input_amount.multiply_ratio(protocol_fee_percent, FEE_DECIMAL),
        input_amount.multiply_ratio(dao_fee_percent, FEE_DECIMAL),
    ))
}

fn get_amount_for_asset(coins: &[Coin], denom: &str) -> Coin {
    let mut amount = Uint128::zero();
    for item in coins {
        if item.denom == denom {
            amount = amount.add(item.amount);
        }
    }
    Coin {
        amount,
        denom: denom.to_string(),
    }
}

pub fn query<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    msg: QueryMsg,
) -> StdResult<Binary> {
    match msg {
        QueryMsg::Config {} => to_binary(&query_config(deps)?),
        QueryMsg::PoolInfo {
            token_a_address,
            token_b_address,
        } => to_binary(&query_pool_info(deps, token_a_address, token_b_address)?),
        QueryMsg::Pools {} => to_binary(&query_pools(deps)?),
        QueryMsg::QueryTokenAForTokenBPrice {
            token_a_address,
            token_b_address,
            token_a_amount,
        } => to_binary(&query_token_a_for_token_b_price(
            deps,
            token_a_address,
            token_b_address,
            token_a_amount,
        )?),
        QueryMsg::QueryTokenBForTokenAPrice {
            token_a_address,
            token_b_address,
            token_b_amount,
        } => to_binary(&query_token_b_for_token_a_price(
            deps,
            token_a_address,
            token_b_address,
            token_b_amount,
        )?),
        QueryMsg::QueryTrFees { token_address } => to_binary(&query_tr_fees(deps, token_address)?),
        QueryMsg::QueryPairTrFees {
            token_a_address,
            token_b_address,
        } => to_binary(&query_pair_tr_fees(deps, token_a_address, token_b_address)?),
    }
}

pub fn query_pool_info<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    token_a_address: HumanAddr,
    token_b_address: HumanAddr,
) -> StdResult<PoolInfoResponse> {
    // load config info
    let config = read_config(&deps.storage)?;
    // check token_a/token_b pool is exixts
    let mut pair_info: Vec<HumanAddr> = Vec::new();
    pair_info.push(token_a_address.clone());
    pair_info.push(token_b_address.clone());
    pair_info.sort_by(|a, b| a.0.as_bytes().cmp(&b.0.as_bytes()));
    let pool_map = read_pool_by_address(&deps.storage, &pair_info[0], &pair_info[1]);
    if !pool_map.is_ok() {
        return Err(StdError::generic_err("Pool is not exist"));
    }
    let pool_info = pool_map.unwrap();
    let lp_token_address = match pool_info.lp_token_asset.clone() {
        AssetInfo::Token { contract_addr, .. } => contract_addr,
        AssetInfo::NativeToken { denom: _ } => HumanAddr::from(""),
    };

    // TODO get total supply
    Ok(PoolInfoResponse {
        token_a_asset: pool_info.token_a_asset,
        token_a_reserve: pool_info.token_a_reserve,
        token_b_asset: pool_info.token_b_asset,
        token_b_reserve: pool_info.token_b_reserve,
        lp_token_supply: query_lp_supply(&deps, &lp_token_address, &config.lp_token_code_hash)?,
        lp_token_asset: pool_info.lp_token_asset.clone(),
        creator: pool_info.creator,
        create_block: pool_info.create_block,
    })
}

fn query_pools<S: Storage, A: Api, Q: Querier>(deps: &Extern<S, A, Q>) -> StdResult<Vec<PoolInfo>> {
    Ok(read_pools(&deps)?)
}

fn query_tr_fees<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    token_address: HumanAddr,
) -> StdResult<(u64, u64)> {
    Ok((
        read_tr_fees(&deps.storage, &token_address, FeesType::In),
        read_tr_fees(&deps.storage, &token_address, FeesType::Out),
    ))
}

fn query_pair_tr_fees<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    token_a_address: HumanAddr,
    token_b_address: HumanAddr,
) -> StdResult<(u64, u64)> {
    Ok((
        read_tr_fees(&deps.storage, &token_a_address, FeesType::In),
        read_tr_fees(&deps.storage, &token_b_address, FeesType::Out),
    ))
}

pub fn query_token_a_for_token_b_price<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    token_a_address: HumanAddr,
    token_b_address: HumanAddr,
    token_a_amount: Uint128,
) -> StdResult<TokenAForTokenBPriceResponse> {
    // load config info
    let config = read_config(&deps.storage)?;
    // check token_a/token_b pool is exixts
    let pool_map = read_pool_by_address(&deps.storage, &token_a_address, &token_b_address);
    if !pool_map.is_ok() {
        return Err(StdError::generic_err("Pool is not exist"));
    }
    let pool_info = pool_map.unwrap();
    let total_fee_percent = config.lp_fee_percent + config.protocol_fee_percent;
    let token_b_amount = get_token_out_amount(
        token_a_amount,
        pool_info.token_a_reserve,
        pool_info.token_b_reserve,
        total_fee_percent,
    )?;

    Ok(TokenAForTokenBPriceResponse { token_b_amount })
}

pub fn query_token_b_for_token_a_price<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    token_a_address: HumanAddr,
    token_b_address: HumanAddr,
    token_b_amount: Uint128,
) -> StdResult<TokenBForTokenAPriceResponse> {
    // load config info
    let config = read_config(&deps.storage)?;
    // check token_a/token_b pool is exixts
    let pool_map = read_pool_by_address(&deps.storage, &token_a_address, &token_b_address);
    if !pool_map.is_ok() {
        return Err(StdError::generic_err("Pool is not exist"));
    }
    let pool_info = pool_map.unwrap();
    let total_fee_percent = config.lp_fee_percent + config.protocol_fee_percent;
    let token_a_amount = get_token_out_amount(
        token_b_amount,
        pool_info.token_b_reserve,
        pool_info.token_a_reserve,
        total_fee_percent,
    )?;

    Ok(TokenBForTokenAPriceResponse { token_a_amount })
}

pub fn query_config<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
) -> StdResult<ConfigInfo> {
    // load config info
    let config = read_config(&deps.storage)?;

    Ok(config)
}

pub fn query_lp_supply<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    contract_addr: &HumanAddr,
    contract_hash: &String,
) -> StdResult<Uint128> {
    // load price form the oracle
    let token_info = token_info_query(
        &deps.querier,
        BLOCK_SIZE,
        contract_hash.clone(),
        contract_addr.clone(),
    )?;

    if token_info.total_supply.is_none() {
        return Ok(Uint128::zero());
    }

    Ok(token_info.total_supply.unwrap())
}

fn get_token_balance<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    contract_addr: &HumanAddr,
    contract_hash: &String,
    addr: &HumanAddr,
    view_key: &String,
) -> StdResult<Uint128> {
    let balance = balance_query(
        &deps.querier,
        addr.clone(),
        view_key.to_string(),
        BLOCK_SIZE,
        contract_hash.clone(),
        contract_addr.clone(),
    )?;

    Ok(balance.amount)
}
